// SPDX-License-Identifier: MIT
#include <stdio.h>
#include <string.h>
#include <stdlib.h> // free
#include "wc/wc_selftest.h"
#include "wc/wc_fold.h"
#include "wc/wc_redirect.h"
#include "wc/wc_server.h"
#include "wc/wc_lookup.h"
#include "wc/wc_net.h"
#include "wc/wc_dns.h"
extern struct Config {
    int whois_port; size_t buffer_size; int max_retries; int timeout_sec; int retry_interval_ms; int retry_jitter_ms; size_t dns_cache_size; size_t connection_cache_size; int cache_timeout; int debug; int max_redirects; int no_redirect; int plain_mode; int fold_output; char* fold_sep; int fold_upper; int security_logging; int fold_unique; int dns_neg_ttl; int dns_neg_cache_disable; int ipv4_only; int ipv6_only; int prefer_ipv4; int prefer_ipv6;
    int dns_addrconfig; int dns_retry; int dns_retry_interval_ms; int dns_max_candidates; int no_dns_known_fallback; int no_dns_force_ipv4_fallback; int no_iana_pivot;
} g_config;
// Optional lookup-specific selftests (non-fatal, guarded by WHOIS_LOOKUP_SELFTEST)
int wc_selftest_lookup(void);

// (env-free version) no local helpers needed

// Lightweight harness network scenario validation without performing real external connects.
// We simulate authoritative / redirect decisions using crafted bodies and minimal wc_lookup_execute
// invocation when possible. For full end-to-end validation a remote smoke test is still required.

static int scenario_chain_tests(void) {
    int failed = 0;
    // We focus on redirect decision heuristics using is_authoritative_response / needs_redirect.
    // Scenario 1: ARIN direct should be authoritative (no IANA pivot) for typical ARIN body.
    const char* arin_body = "NetRange: 8.8.8.0 - 8.8.8.255\nOrgName: Google LLC\n";
    if (!is_authoritative_response(arin_body)) { fprintf(stderr, "[SELFTEST] scenario1-arin-authoritative: FAIL\n"); failed=1; }
    else fprintf(stderr, "[SELFTEST] scenario1-arin-authoritative: PASS\n");

    // Scenario 2: Simulate ARIN IPv4 blocked fallback via needs_redirect phrase (treated as redirect) then authoritative.
    // We cannot force network failure here; instead we validate redirect trigger phrase exists and authoritative phrase afterwards.
    const char* arin_block_phrase = "No match found for 199.91.0.46"; // sample placeholder
    if (!needs_redirect(arin_block_phrase)) { fprintf(stderr, "[SELFTEST] scenario2-arin-fallback-trigger: FAIL\n"); failed=1; }
    else fprintf(stderr, "[SELFTEST] scenario2-arin-fallback-trigger: PASS\n");

    // Scenario 3: IANA referral to ARIN: body shows Refer: whois.arin.net and is not itself authoritative for 8.8.8.8
    const char* iana_body = "refer: whois.arin.net\nwhois: data\n"; // lowercase refer acceptable
    if (!needs_redirect(iana_body)) { fprintf(stderr, "[SELFTEST] scenario3-iana-to-arin-redirect: FAIL\n"); failed=1; }
    else fprintf(stderr, "[SELFTEST] scenario3-iana-to-arin-redirect: PASS\n");

    // Scenario 4: IANA referral to APNIC (simulate 1.1.1.1 path)
    const char* iana_to_apnic = "Refer: whois.apnic.net\n";
    if (!needs_redirect(iana_to_apnic)) { fprintf(stderr, "[SELFTEST] scenario4-iana-to-apnic-redirect: FAIL\n"); failed=1; }
    else fprintf(stderr, "[SELFTEST] scenario4-iana-to-apnic-redirect: PASS\n");

    // Scenario 5: Empty-body fallback simulation – inject environment flag and ensure warning path reachable.
    // We trigger the code path by calling wc_lookup_execute on a query with WHOIS_SELFTEST_INJECT_EMPTY=1.
    // Since real network I/O is out of scope here (wc_dial_43 would attempt a connect), we guard with fast skip if dial fails.
    wc_selftest_set_inject_empty(1);
    struct wc_query q = { .raw = "8.8.8.8", .start_server = "whois.iana.org", .port = 43};
    struct wc_lookup_opts o = { .max_hops=2, .no_redirect=1, .timeout_sec=1, .retries=0 };
    struct wc_result r; memset(&r,0,sizeof(r));
    int lrc = wc_lookup_execute(&q, &o, &r);
    if (lrc == 0 && r.body && strstr(r.body, "Warning: empty response") ) {
        fprintf(stderr, "[SELFTEST] scenario5-empty-body-injection: PASS\n");
    } else {
        // If network unavailable (failure) we treat as inconclusive rather than FAIL to avoid false negatives in offline builds.
        if (lrc == 0) { fprintf(stderr, "[SELFTEST] scenario5-empty-body-injection: FAIL (no warning)\n"); failed=1; }
        else fprintf(stderr, "[SELFTEST] scenario5-empty-body-injection: SKIP (dial failure)\n");
    }
    wc_lookup_result_free(&r);
    // Scenario 6: LACNIC empty-body injection (single retry budget). We only check that if warning appears it is PASS.
    // Host whois.lacnic.net
    wc_selftest_set_inject_empty(1);
    struct wc_query q2 = { .raw = "2800:1:200::", .start_server = "whois.lacnic.net", .port = 43};
    struct wc_lookup_opts o2 = { .max_hops=1, .no_redirect=1, .timeout_sec=1, .retries=0 };
    struct wc_result r2; memset(&r2,0,sizeof(r2));
    int lrc2 = wc_lookup_execute(&q2, &o2, &r2);
    if (lrc2 == 0 && r2.body && strstr(r2.body, "Warning: empty response") ) {
        fprintf(stderr, "[SELFTEST] scenario6-lacnic-empty-body-injection: PASS\n");
    } else {
        if (lrc2 == 0) { fprintf(stderr, "[SELFTEST] scenario6-lacnic-empty-body-injection: FAIL (no warning)\n"); failed=1; }
        else fprintf(stderr, "[SELFTEST] scenario6-lacnic-empty-body-injection: SKIP (dial failure)\n");
    }
    wc_lookup_result_free(&r2);
    wc_selftest_set_inject_empty(0);
    return failed ? 1 : 0;
}

static void selftest_dns_candidate_limit(void) {
    int prev_limit = g_config.dns_max_candidates;
    g_config.dns_max_candidates = 1;
    wc_dns_candidate_list_t list = {0};
    int rc = wc_dns_build_candidates("whois.arin.net", "arin", &list);
    if (rc != 0 || list.count == 0) {
        fprintf(stderr, "[SELFTEST] dns-cand-limit: SKIP (resolver unavailable)\n");
    } else {
        int pass = (list.count <= 1);
        fprintf(stderr, "[SELFTEST] dns-cand-limit: %s (count=%d)\n", pass ? "PASS" : "WARN", list.count);
    }
    wc_dns_candidate_list_free(&list);
    g_config.dns_max_candidates = prev_limit;
}

static void selftest_dns_negative_flag(void) {
    wc_dns_candidate_list_t list = {0};
    wc_selftest_set_dns_negative(1);
    int rc = wc_dns_build_candidates("selftest.invalid", "unknown", &list);
    if (rc == 0 && list.count > 0) {
        fprintf(stderr, "[SELFTEST] dns-neg-cache: WARN (unexpected candidates)\n");
    } else {
        fprintf(stderr, "[SELFTEST] dns-neg-cache: PASS (gai_err=%d)\n", list.last_error);
    }
    wc_dns_candidate_list_free(&list);
    wc_selftest_set_dns_negative(0);
}

static int selftest_dns_family_controls(void) {
    const char* literal = "2001:db8::cafe";
    int prev_ipv4 = g_config.ipv4_only;
    int prev_ipv6 = g_config.ipv6_only;
    int prev_pref4 = g_config.prefer_ipv4;
    int prev_pref6 = g_config.prefer_ipv6;

    // Scenario A: IPv6-only should suppress canonical host fallback (numeric-only list)
    g_config.ipv4_only = 0;
    g_config.ipv6_only = 1;
    g_config.prefer_ipv4 = 0;
    g_config.prefer_ipv6 = 0;
    wc_dns_candidate_list_t list = {0};
    int rc = wc_dns_build_candidates(literal, "arin", &list);
    int failed_local = 0;
    if (rc != 0) {
        fprintf(stderr, "[SELFTEST] dns-ipv6-only-candidates: SKIP (rc=%d last_error=%d)\n", rc, list.last_error);
    } else {
        int pass = (list.count == 1 && list.items && list.items[0] && strcmp(list.items[0], literal) == 0);
        if (pass && list.origins) {
            pass = (list.origins[0] == (unsigned char)WC_DNS_ORIGIN_INPUT);
        }
        fprintf(stderr, "[SELFTEST] dns-ipv6-only-candidates: %s\n", pass ? "PASS" : "FAIL");
        if (!pass) {
            failed_local = 1;
            fprintf(stderr, "  details: count=%d literal=%s origin=%u\n", list.count,
                    (list.items && list.items[0]) ? list.items[0] : "(null)",
                    (list.origins ? list.origins[0] : 255));
        }
    }
    wc_dns_candidate_list_free(&list);

    // Scenario B: default family preference allows canonical host fallback to reappear
    g_config.ipv6_only = 0;
    g_config.prefer_ipv6 = 1;
    list = (wc_dns_candidate_list_t){0};
    rc = wc_dns_build_candidates(literal, "arin", &list);
    if (rc != 0) {
        fprintf(stderr, "[SELFTEST] dns-canonical-fallback: SKIP (rc=%d last_error=%d)\n", rc, list.last_error);
    } else {
        const char* canon = wc_dns_canonical_host_for_rir("arin");
        int found = 0;
        for (int i = 0; i < list.count; ++i) {
            if (list.items && list.items[i] && canon && strcmp(list.items[i], canon) == 0) {
                if (!list.origins || list.origins[i] == (unsigned char)WC_DNS_ORIGIN_CANONICAL) {
                    found = 1;
                    break;
                }
            }
        }
        fprintf(stderr, "[SELFTEST] dns-canonical-fallback: %s\n", found ? "PASS" : "FAIL");
        if (!found) failed_local = 1;
    }
    wc_dns_candidate_list_free(&list);

    g_config.ipv4_only = prev_ipv4;
    g_config.ipv6_only = prev_ipv6;
    g_config.prefer_ipv4 = prev_pref4;
    g_config.prefer_ipv6 = prev_pref6;
    return failed_local;
}

static int selftest_dns_fallback_toggles(void) {
    int prev_force = g_config.no_dns_force_ipv4_fallback;
    int prev_known = g_config.no_dns_known_fallback;

    struct wc_query q = { .raw = "8.8.8.8", .start_server = "whois.arin.net", .port = 43 };
    struct wc_lookup_opts opts = { .max_hops = 1, .no_redirect = 1, .timeout_sec = 1, .retries = 0 };
    struct wc_result r; memset(&r, 0, sizeof(r));

    wc_selftest_set_blackhole_arin(1);

    g_config.no_dns_force_ipv4_fallback = 0;
    g_config.no_dns_known_fallback = 0;
    wc_selftest_reset_dns_fallback_counters();
    int rc = wc_lookup_execute(&q, &opts, &r);
    int forced_attempts = wc_selftest_forced_ipv4_attempts();
    int known_attempts = wc_selftest_known_ip_attempts();
    int failed_local = 0;
    if (forced_attempts > 0 && known_attempts > 0) {
        fprintf(stderr, "[SELFTEST] dns-fallback-enabled: PASS (forced=%d known=%d rc=%d)\n",
                forced_attempts, known_attempts, rc);
    } else {
        fprintf(stderr, "[SELFTEST] dns-fallback-enabled: FAIL (forced=%d known=%d rc=%d)\n",
                forced_attempts, known_attempts, rc);
        failed_local = 1;
    }
    wc_lookup_result_free(&r);

    g_config.no_dns_force_ipv4_fallback = 1;
    g_config.no_dns_known_fallback = 1;
    wc_selftest_reset_dns_fallback_counters();
    memset(&r, 0, sizeof(r));
    int rc2 = wc_lookup_execute(&q, &opts, &r);
    forced_attempts = wc_selftest_forced_ipv4_attempts();
    known_attempts = wc_selftest_known_ip_attempts();
    if (forced_attempts == 0 && known_attempts == 0) {
        fprintf(stderr, "[SELFTEST] dns-fallback-disabled: PASS (rc=%d)\n", rc2);
    } else {
        fprintf(stderr, "[SELFTEST] dns-fallback-disabled: FAIL (forced=%d known=%d rc=%d)\n",
                forced_attempts, known_attempts, rc2);
        failed_local = 1;
    }
    wc_lookup_result_free(&r);

    wc_selftest_set_blackhole_arin(0);
    g_config.no_dns_force_ipv4_fallback = prev_force;
    g_config.no_dns_known_fallback = prev_known;
    return failed_local;
}

int wc_selftest_run(void) {
    int failed = 0;

    // Network chain scenario checks (redirect heuristics + empty-body fallback injection)
    int sc = scenario_chain_tests();
    if (sc != 0) failed = 1;

    // Fold basic test
    const char* body = "netname: Google\n descr: Alpha\n descr: Alpha\n    Mountain View\n";
    char* s1 = wc_fold_build_line(body, "8.8.8.8", "whois.arin.net", " ", 1);
    if (!s1 || strstr(s1, "GOOGLE") == NULL) { fprintf(stderr, "[SELFTEST] fold-basic: FAIL\n"); failed = 1; }
    else fprintf(stderr, "[SELFTEST] fold-basic: PASS\n");
    if (s1) free(s1);

    // Fold unique test
    wc_fold_set_unique(1);
    char* s2 = wc_fold_build_line(body, "8.8.8.8", "whois.arin.net", ",", 1);
    if (!s2) { fprintf(stderr, "[SELFTEST] fold-unique: FAIL (null)\n"); failed = 1; }
    else {
        int has_dup = strstr(s2, ",ALPHA,ALPHA,") != NULL;
        if (has_dup) { fprintf(stderr, "[SELFTEST] fold-unique: FAIL (dup) -> %s\n", s2); failed = 1; }
        else fprintf(stderr, "[SELFTEST] fold-unique: PASS\n");
    }
    if (s2) free(s2);
    wc_fold_set_unique(0);

    // Redirect: needs_redirect basic phrases
    const char* redir_samples[] = {
        "No match found for 1.2.3.4",            // no match
        "This block is UNALLOCATED",             // unallocated
        "not registered in LACNIC",              // not registered
        "Refer: whois.ripe.net",                 // refer:
        NULL
    };
    for (int i = 0; redir_samples[i]; i++) {
        if (!needs_redirect(redir_samples[i])) { fprintf(stderr, "[SELFTEST] redirect-detect-%d: FAIL\n", i); failed = 1; } 
        else fprintf(stderr, "[SELFTEST] redirect-detect-%d: PASS\n", i);
    }

    // Redirect: is_authoritative_response indicators
    const char* auth_sample = "inetnum: 8.8.8.0 - 8.8.8.255\nnetname: GOOGLE\ncountry: US\n";
    if (!is_authoritative_response(auth_sample)) { fprintf(stderr, "[SELFTEST] auth-indicators: FAIL\n"); failed = 1; }
    else fprintf(stderr, "[SELFTEST] auth-indicators: PASS\n");

    // Redirect: extract_refer_server basic
    const char* ex1 = "ReferralServer: whois://whois.ripe.net\n";
    char* rs = extract_refer_server(ex1);
    if (!rs || strcmp(rs, "whois.ripe.net") != 0) { fprintf(stderr, "[SELFTEST] extract-refer: FAIL (%s)\n", rs ? rs : "null"); failed = 1; }
    else fprintf(stderr, "[SELFTEST] extract-refer: PASS\n");
    if (rs) free(rs);

    // Server normalize + RIR guess (light sanity)
    char hostbuf[64];
    if (wc_normalize_whois_host("ripe", hostbuf, sizeof(hostbuf)) != 0 || strcmp(hostbuf, "whois.ripe.net") != 0) {
        fprintf(stderr, "[SELFTEST] server-normalize: FAIL (%s)\n", hostbuf);
        failed = 1;
    } else {
        fprintf(stderr, "[SELFTEST] server-normalize: PASS\n");
    }
    const char* rir = wc_guess_rir("whois.arin.net");
    if (!rir || strcmp(rir, "arin") != 0) { fprintf(stderr, "[SELFTEST] rir-guess: FAIL (%s)\n", rir ? rir : "null"); failed = 1; }
    else fprintf(stderr, "[SELFTEST] rir-guess: PASS\n");

#ifdef WHOIS_GREP_TEST
    fprintf(stderr, "[SELFTEST] grep: BUILT-IN TESTS ENABLED (run at startup if env set)\n");
#else
    fprintf(stderr, "[SELFTEST] grep: not built (compile with -DWHOIS_GREP_TEST to enable)\n");
#endif

#ifdef WHOIS_SECLOG_TEST
    fprintf(stderr, "[SELFTEST] seclog: BUILT-IN TESTS ENABLED (run at startup if env set)\n");
#else
    fprintf(stderr, "[SELFTEST] seclog: not built (compile with -DWHOIS_SECLOG_TEST to enable)\n");
#endif

    // Lookup suite (weak)
    wc_selftest_lookup();

    selftest_dns_candidate_limit();
    selftest_dns_negative_flag();
    failed |= selftest_dns_family_controls();
    failed |= selftest_dns_fallback_toggles();

    return failed ? 1 : 0;
}

